package sketch.ounit.fuzz;

import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;

import randoop.ComponentManager;
import randoop.ExecutableSequence;
import randoop.ExecutionOutcome;
import randoop.NormalExecution;
import randoop.RMethod;
import randoop.Sequence;
import randoop.StatementKind;
import randoop.Variable;
import randoop.main.GenInputsAbstract;
import randoop.util.MethodReflectionCode;
import randoop.util.Reflection;
import randoop.util.ReflectionCode;
import randoop.util.ReflectionExecutor;
import randoop.util.SimpleList;
import sketch.util.Checker;
import sketch.util.Globals;
import sketch.util.Randomness;
import sketch.util.RandoopUtil;
import sketch.util.SketchLog;

//this class is immutable!
/**
 * Potential problems:
 * 1. this class may fail to create sufficient valid sequence for a program
 *    with constrained interfaces
 *    The sequence generation mechanism is slightly different from randoop. it
 *    tries to append multiple methods simentalously instead of one method once.
 * 2. this class may produce redundant tests, two possible ways:
 *    first, the textual comparison
 *    second, use the equals method
 * */
public class AssembledSequenceObjectMutation<T> implements AssembledSequence<T> {
	
	protected static int max_extend_times = 10;
	public static void setMaxExtendTimes(int maxtime) {
		Checker.checkTrue(maxtime > 0, "The maxtime should be > 0");
		max_extend_times = maxtime;
	}
	
	protected static int max_repetition_times = 50;
	public static void setMaxRepetitionTimes(int maxtime) {
		Checker.checkTrue(maxtime > 0, "The maxtime should be > 0");
		max_repetition_times = maxtime;
	}
	
	//the following fields are initialized inside the constructor
	protected final List<RMethod> all_methods;
	protected final T recevier;
	protected final Class<?> receiver_class;
	protected final ComponentManager components;
	protected final int extend_times;
	
	//the result object and possible thrown exception in this class
	protected T result_object;
	protected Throwable thrown_exp = null;
	public boolean hasException() {
		return thrown_exp != null;
	}
	public Throwable getException() {
		return thrown_exp;
	}
	
	/**
	 * The following three fields are intialized through a method
	 * */
	//the method list to extend
	protected List<RMethod> extended_methods = new LinkedList<RMethod>();
	//the sequence selected as parameters
	protected List<List<ExecutableSequence>> method_params = new LinkedList<List<ExecutableSequence>>();
	//the indices
	protected List<List<Integer>> param_indices = new LinkedList<List<Integer>>();
	//the repetitions
	protected List<Integer> repetitions = new LinkedList<Integer>();
	
	//a tag for processing, removing redundant sequence
	protected boolean is_redundant = false;
	public void makeItRedundant() {
		this.is_redundant = true;
	}
	public boolean isRedundant() {
		return this.is_redundant;
	}
	
	/*The field for output*/
	protected int offset = 0;
	protected String receiver_name = null;
	protected String[] arg_names = null;
	protected String[] expression_names = null;
	public void setOutputParams(int offset, String receiver_name, String[] arg_names, String[] expr_names) {
		Checker.checkTrue(offset > -1, "The offset is not valid: " + offset);
		this.offset = offset;
		this.receiver_name = receiver_name;
		this.arg_names = arg_names;
		this.expression_names = expr_names;
	}
	
	@Override
	public String toCodeString(int offset, String receiver, String[] argnames, String[] expressionNames) {
		return this.toCodeString(offset, receiver);
	}

	@Override
	public String getResultVar(int offset) {
		return "%Sequence_Mutation_No_Var%";
	}
	
	@Override
	public int size() {
		int size = 0;
		for(List<ExecutableSequence> method_param : this.method_params) {
			for(ExecutableSequence seq : method_param) {
				size += seq.sequence.size();
			}
		}
		return size;
	}
	
	public AssembledSequenceObjectMutation(List<RMethod>  rmethods, T receiver, Class<?> clazz, ComponentManager components) {
		Checker.checkNull(rmethods, "The rmethods could not be null.");
		this.all_methods = rmethods;
		Checker.checkNull(components, "The component manager could not be null.");
		this.components = components;
		//set up the extend_times
		this.extend_times = Randomness.nextRandomInt(max_extend_times) + 1;
		//receiver could be null for static
		this.recevier = receiver;
		Checker.checkNull(clazz, "The reciever class could not be null");
		this.receiver_class = clazz;
		//add a default name
		this.receiver_name = "%receiver_name%";
	}
	
	public void execute_sequence() {
		//assemble the sequence and execute it
		this.result_object = this.assemble_and_execute_sequence();
		//check the validity
		this.checkAssembleSequenceValidity();
	}
	
	public T getResultObject() {
		return this.result_object;
	}
	
	public String toCodeString() {
		this.checkToCodeStringSetUp();
		return this.toCodeString(this.offset, this.receiver_name);
	}
	
	protected void checkToCodeStringSetUp() {
		Checker.checkTrue(this.offset > -1, "The offset is not valid: " + this.offset + " call setOutputParams first.");
		Checker.checkNull(this.receiver_name, "The receiver name should not be null. call setOutputParams first.");
	}
	
	/**
	 * Converts the assembled sequence to code string
	 * */
	String toCodeString(String receiver_name) {
		return this.toCodeString(0, receiver_name);
	}
	
	String toCodeString(int offset, String receiver_name) {
		Checker.checkNull(receiver_name, "The receiver name could not be null.");
		this.checkAssembleSequenceValidity();
		//concatenate all
		List<Sequence> all_sequences = new LinkedList<Sequence>();
		for(List<ExecutableSequence> seq_list : this.method_params) {
			for(ExecutableSequence eSeq : seq_list) {
				all_sequences.add(eSeq.sequence);
			}
		}
		Sequence concatenate = Sequence.concatenate(all_sequences);
		//append selected methods inside the concatenated sequence
		StringBuilder sb = new StringBuilder();
		int stmt_index = 0;
		int sequence_length = 0;
		for(int i = 0; i < this.method_params.size(); i++) {
			boolean isTheLastMethod = (i == this.method_params.size() - 1);
			//get the invoke method
			RMethod rmethod = this.extended_methods.get(i);
			Method method = rmethod.getMethod();
			List<ExecutableSequence> seq_list = this.method_params.get(i);
			List<Integer> indices = this.param_indices.get(i);
			int repetition = this.repetitions.get(i);
			Checker.checkTrue(repetition > 0, "repetition: " + repetition);
			//first append, all the parameters
			int length = 0;
			for(ExecutableSequence eseq : seq_list) {
				length = length + eseq.sequence.size();
			}
			for(int append_index = stmt_index; append_index < stmt_index + length; append_index++) {
//				concatenate.printStatement(sb, append_index);
				//rename it
				RandoopUtil.appendSequenceStatementWithOffset(offset, concatenate, sb, append_index);
			}
			//append the invoked method
			List<Variable> vars = new LinkedList<Variable>();
			int current_index = 0;
			for(int idx = 0; idx < indices.size(); idx++) {
				int relative_idx = indices.get(idx);
				current_index = sequence_length + relative_idx;
				Variable var = concatenate.getVariable(current_index);
				sequence_length += seq_list.get(idx).sequence.size();
				vars.add(var);
			}
			while(repetition -- != 0) {
			  //check to see if any exception wrapping is needed
			  boolean needToWrap = ((repetition == 0) && this.hasException() && isTheLastMethod);
			  if(needToWrap) {
				  Checker.checkNull(this.getException(), "The thrown exception should not be null.");
				  sb.append("try {");
				  sb.append(Globals.lineSep);
			  } //else if (repetition == 0) {
				//  Checker.checkTrue(this.getException() == null, "The thrown exception should be null.");
			  //}
			  //print out the method
			  if(rmethod.isStatic()) {
				sb.append(this.receiver_class.getName());
			  } else {
				  sb.append(receiver_name);
			  }
			  sb.append(".");
			  sb.append(method.getName());
			  sb.append("(");
			  for(int varIndex = 0; varIndex < vars.size(); varIndex++) {
			    if(varIndex!=0) {
				  sb.append(", ");
				}
			    Variable var = vars.get(varIndex);
			    //do the renmaing
			    var = new Variable(var.sequence, var.index + offset);
				sb.append(var.getName());
			  }
			  sb.append(");");
			  sb.append(Globals.lineSep);
			  if(needToWrap) {
				sb.append("  fail(\" exception: " + this.getException().toString() + " should be thrown \");" );
				sb.append(Globals.lineSep);
			    sb.append("} catch (" + this.getException().getClass().getName() + " e) {");
			    sb.append(Globals.lineSep);
			    sb.append("  //ok here");
			    sb.append(Globals.lineSep);
			    sb.append("}");
			    sb.append(Globals.lineSep);
			  }
			}
			
			//adjust the statement index
			stmt_index = stmt_index + length;
		}
		
		return sb.toString();
	}
	
	//default is using thread to execute it, that is, if the execution can not
	//terminate in a certain time limit. the tool will kill the thread
	@Override
	public T assemble_and_execute_sequence() {
		int method_extended = 0;
		int sequence_length = 0;
		T receiver_obj = this.recevier;
		//pick a number of methods
		while(method_extended < this.extend_times && sequence_length < GenInputsAbstract.maxsize) {
			RMethod rmethod = Randomness.randomMember(this.all_methods);
			Method method = rmethod.getMethod();
			//find sequences for each method
			List<ExecutableSequence> params = new LinkedList<ExecutableSequence>();
			List<Integer> param_indices = new LinkedList<Integer>();
			Class<?>[] required_types = method.getParameterTypes();
			//the object array
			Object[] param_objs = new Object[required_types.length];
			//choose sequences for each required type
			int param_index = 0;
			for(Class<?> required_type : required_types) {
				SimpleList<Sequence> list = this.components.getSequencesForType(required_type, false);
				if(list.isEmpty()) {
					SketchLog.logln("The list is empty, no sequence found for type: " + required_type);
					//return the current receiver_obj
					return receiver_obj;
				}
				Sequence seq = Randomness.randomMember(list.toJDKList());
				ExecutableSequence eSeq = new ExecutableSequence(seq);
				eSeq.execute(null);
				//check which index could be used
				List<Variable> lastVars = seq.getLastStatementVariables();
				List<Variable> all_compatibles = new LinkedList<Variable>();
				for(Variable var : lastVars) {
					Class<?> var_type = seq.getStatementKind(var.getDeclIndex()).getOutputType();
					if(Reflection.canBeUsedAs(var_type, required_type)) {
						all_compatibles.add(var);
					}
				}
				Checker.checkTrue(!all_compatibles.isEmpty(), "The compatibles should not be empty.");
				Variable var = Randomness.randomMember(all_compatibles);
				params.add(eSeq);
				param_indices.add(var.getDeclIndex());
				//set the param
				ExecutionOutcome outcome = eSeq.getResult(var.getDeclIndex());
				Checker.checkTrue(outcome instanceof NormalExecution, "The outcome should be a normal execution");
				param_objs[param_index++] = ((NormalExecution)outcome).getRuntimeValue();
			}
			int repeat_times = 1;
			if(Randomness.nextRandomInt(10) == 0) {
				repeat_times = Randomness.nextRandomInt(max_repetition_times) + 1;
			}
			
			//execute it for n times
			Throwable thrown_exception = null;
			for(int time = 1; time <=repeat_times; time++) {
				ReflectionCode code = new MethodReflectionCode(method, receiver_obj, param_objs);
				Throwable e = ReflectionExecutor.executeReflectionCode(code, System.out);
				if(e != null) {
					thrown_exception = e;
					repeat_times = time;
					//XXX break or not?
					break;
				}
			}
			//add to the sequence
			this.extended_methods.add(rmethod);
			this.method_params.add(params);
			this.param_indices.add(param_indices);
			this.repetitions.add(repeat_times);
			//if there is no exception we continue, or else we break;
			if(thrown_exception != null) {
				Checker.checkTrue(this.thrown_exp == null, "The current thrown exception should be null.");
				this.thrown_exp = thrown_exception;
				break;
			}
			//the method number update
			method_extended++;
			for(ExecutableSequence param_seq : params) {
			  sequence_length += param_seq.sequence.size(); 
			}
		}//continue to select another method
		
		return receiver_obj;
	}
	
	protected void checkAssembleSequenceValidity() {
		Checker.checkTrue(this.extended_methods.size() == this.method_params.size(), "The extended method size: "
				+ extended_methods.size() + " != the size of method parameters: " + this.method_params.size());
		Checker.checkTrue(this.method_params.size() == this.param_indices.size(), "The extended method params size: "
				+ method_params.size() + " != the size of param indices: " + param_indices.size());
		for(int i = 0; i < this.extended_methods.size(); i++) {
			RMethod rmethod = this.extended_methods.get(i);
			List<ExecutableSequence> params = this.method_params.get(i);
			List<Integer> indices = this.param_indices.get(i);
			int repetition = this.repetitions.get(i);
			Checker.checkTrue(repetition >= 1, "Invalid repetition value: " + repetition);
			//check each pair
			checkParamValidity(rmethod, params, indices);
		}
	}
	
	//check the validity of parameters
	protected void checkParamValidity(RMethod rmethod, List<ExecutableSequence> params, List<Integer> indices) {
		Class<?>[] rmethod_input_types = rmethod.getMethod().getParameterTypes();//getInputTypes();
		Checker.checkTrue(params.size() == rmethod_input_types.length, "The provided param length: " + params.size()
				+ " != the size of rmethod input types: "+ rmethod_input_types.length);
		Checker.checkTrue(params.size() == indices.size(), "The size of params: " + params.size() + " !=  the size" +
				" of indices: " + indices.size());
		
		for(int i = 0; i < params.size(); i++) {
			Class<?> required_type = rmethod_input_types[i];
			int index = indices.get(i);
			ExecutableSequence provided_sequence = params.get(i);
			Checker.checkTrue(!provided_sequence.hasNonExecutedStatements(), "Every statement in: " + i
					+ "-th param should be executed!");
			Checker.checkTrue(index < provided_sequence.sequence.size(), "The index: " + index
					+ " should < the sequence size: " + provided_sequence.sequence.size());
			//get the type
			StatementKind statement = provided_sequence.sequence.getStatementKind(index);
			Class<?> output_type = statement.getOutputType();
			//check the type compatibility
			Checker.checkTrue(Reflection.canBeUsedAs(output_type, required_type), "The output type: " + output_type
					+ " could not be used as requried_type: " + required_type);
		}
	}
	
	/**
	 * The following two overriden methods consider the following methods:
	 * private final Class<?> receiver_class;
	 * private List<RMethod> extended_methods = new LinkedList<RMethod>();
	 * private List<List<ExecutableSequence>> method_params = new LinkedList<List<ExecutableSequence>>();
	 * private List<List<Integer>> param_indices = new LinkedList<List<Integer>>();
	 * private List<Integer> repetitions = new LinkedList<Integer>();
	 * */
	@Override
	public int hashCode() {
		int hashcode = 0;
		hashcode += 3*receiver_class.hashCode();
		for(RMethod rmethod : extended_methods) {
			hashcode += 5*rmethod.hashCode();
		}
		for(List<ExecutableSequence> list : method_params) {
			for(ExecutableSequence eseq : list) {
				hashcode += 13*eseq.hashCode();
			}
		}
		for(List<Integer> indices: param_indices) {
			for(Integer index : indices) {
				hashcode += 17*index;
			}
		}
		for(Integer repetition : repetitions) {
			hashcode += 23*repetition;
		}
		return hashcode;
	}
	
	@Override
	public boolean equals(Object obj) {
		if(!(obj instanceof AssembledSequenceObjectMutation)) {
			return false;
		}
		AssembledSequenceObjectMutation<?> other = (AssembledSequenceObjectMutation<?>)obj;
		return this.receiver_class.equals(other.receiver_class)
		     && this.extended_methods.equals(other.extended_methods)
		     && this.method_params.equals(other.method_params)
		     && this.param_indices.equals(other.param_indices)
		     && this.repetitions.equals(other.repetitions);
	}
	
	@Override
	public String toString() {
		return this.toCodeString("%test_to_string%");
	}
}